20.1 定义
我们用一个简单的示例来揭开defer的秘密。
test.go
package main import () func main() { defer println(0x11) }
反编译:
go tool objdump -s “main.main” test TEXT main.main(SB) test.go test.go:5 0x204f SUBQ 0x11, 0x10(SP) // arg 0x11 test.go:6 0x205c MOVL 0x0, AX test.go:6 0x2077 JNE 0x2084 test.go:7 0x2079 NOPL test.go:7 0x207a CALL runtime.deferreturn(SB) test.go:7 0x207f ADDQ nm test | grep “85808” 0000000000085808 s main.print.1.f
编译器将defer处理成两个函数调用,deferproc定义一个延迟调用对象,然后在函数结束前通过deferreturn完成最终调用。
和前面一样,对于这类参数不确定的都是用funcval处理,siz是目标函数参数长度。
runtime2.go
type _defer struct { siz int32 started bool sp uintptr // 调用 deferproc 时的 SP pc uintptr // 调用 deferproc 时的 IP fn *funcval _panic *_panic // panic that is running defer link *_defer }
panic.go
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn sp := getcallersp(unsafe.Pointer(&siz)) argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc(unsafe.Pointer(&siz)) systemstack(func() { d := newdefer(siz) d.fn = fn d.pc = callerpc d.sp = sp memmove(add(unsafe.Pointer(d), unsafe.Sizeof(*d)), unsafe.Pointer(argp), uintptr(siz)) }) // deferproc returns 0 normally // a deferred func that stops a panic makes the deferproc return 1 // the code the compiler generates always checks the return value and jumps to the // end of the function if deferproc returns != 0 return0() }
这个函数粗看没什么复杂的地方,但有两个问题:第一,参数被复制到了defer对象后面的内存空间;第二,匿名函数中创建的d不知保存在哪里。
panic.go
func newdefer(siz int32) *_defer { var d _defer // 参数长度对齐后,获取缓存等级 sc := deferclass(uintptr(siz)) mp := acquirem() // 未超出缓存大小 if sc < uintptr(len(p{}.deferpool)) { pp := mp.p.ptr() // 如果 P 本地缓存已空,从全局提取一批到本地 if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil { d := sched.deferpool[sc] sched.deferpool[sc] = d.link d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) } } // 从本地缓存尾部提取 if n := len(pp.deferpool[sc]); n > 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } // 新建。很显然分配的空间大小除 _defer 外,还有参数 if d == nil { // Allocate new defer+args. total := roundupsize(totaldefersize(uintptr(siz))) d = (_defer)(mallocgc(total, deferType, 0)) } d.siz = siz // 将 d 保存到 G._defer 链表 gp := mp.curg d.link = gp._defer gp._defer = d releasem(mp) return d }
runtime2.go
type p struct { deferpool [5][]*_defer } type g struct { _defer *_defer }
defer同样使用了二级缓存,这个没兴趣深究。newdefer函数解释了前面的两个问题:一次性为defer和参数分配空间;d被挂到G._defer链表。
那么,退出前deferreturn自然是从G._defer获取并执行延迟函数了。
panic.go
func deferreturn(arg0 uintptr) { gp := getg() // 提取 defer 延迟对象 d := gp._defer if d == nil { return } // 对比 SP,避免调用其他栈帧的延迟函数。(arg0 也就是 deferproc siz 参数) sp := getcallersp(unsafe.Pointer(&arg0)) if d.sp != sp { return } mp := acquirem() // 将延迟函数的参数复制到堆栈(这会覆盖掉 siz、fn,不过没有影响) memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) fn := d.fn d.fn = nil // 调整 G._defer 链表 gp._defer = d.link // 释放 _defer 对象,放回缓存 systemstack(func() { freedefer(d) }) releasem(mp) // 执行延迟函数 jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
freedefer将_defer放回P.deferpool缓存,当数量超出时会转移部分到sched.deferpool。垃圾回收时,clearpools会清理掉sched.deferpool缓存。
汇编实现的jmpdefer函数很有意思。
首先通过arg0参数,也就是调用deferproc时压入的第一参数siz获取main.main SP。当main调用deferreturn时,用SP-8就可以获取当时保存的main IP值。因为IP保存了下一条指令地址,那么用该地址减去CALL指令长度,自然又回到了main调用deferreturn函数的位置。将这个计算得来的地址入栈,加上jmpdefer没有保存现场,那么延迟函数fn RET自然回到CALL deferreturn,如此就实现了多个defer延迟调用循环。
asm_amd64.s
TEXT runtime•jmpdefer(SB), NOSPLIT, 5, (SP) // CALL 指令长度 5,-5 返回的就是 call deferreturn 指令地址 MOVQ 0(DX), BX // 执行 fn 函数 JMP BX
费好大力气,真有必要这么做吗?
虽然整个调用堆栈的defer都挂在G._defer链表,但在deferreturn里面通过sp值的比对,可避免调用其他栈帧的延迟函数。
如中途用Goexit终止,它会负责处理整个调用堆栈的延迟函数。
panic.go
func Goexit() { gp := getg() for { d := gp._defer if d == nil { break } if d.started { if d._panic != nil { d._panic.aborted = true d._panic = nil } d.fn = nil gp._defer = d.link freedefer(d) continue } d.started = true reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) if gp._defer != d { throw(“bad defer entry in Goexit”) } d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) } goexit1() }